package jmine.tec.utils.io.http.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.UnmodifiableTuple;
import jmine.tec.utils.collection.MultiMap;
import jmine.tec.utils.collection.impl.ListMultiMap;
import jmine.tec.utils.io.IOUtils;
import jmine.tec.utils.io.TemporaryFileInputStream;
import jmine.tec.utils.io.http.Connector;
import jmine.tec.utils.io.http.HttpMethod;
import jmine.tec.utils.io.http.exception.HttpException;

/**
 * @author takeshi
 */
public class URLConnector extends AbstractConnector implements Connector, Serializable {

    private final String baseURL;

    private final Map<String, String> cookies = new HashMap<String, String>();

    /**
     * C'tor
     * 
     * @param baseURL String
     */
    public URLConnector(String baseURL) {
        super();
        this.baseURL = baseURL.endsWith("/") ? baseURL.substring(0, baseURL.length() - 1) : baseURL;
    }

    /**
     * {@inheritDoc}
     */
    public void close() {
    }

    /**
     * {@inheritDoc}
     */
    public void connect() throws IOException, HttpException {
    }

    /**
     * {@inheritDoc}
     */
    public boolean isConnected() {
        return true;
    }

    /**
     * Gera uma excecao se a resposta nao for um http 200
     * 
     * @param path String
     * @param params mapa com parametros
     * @param connection a {@link HttpURLConnection}
     * @param responseHeader the response header fields
     * @throws IOException e
     * @throws HttpException e
     */
    protected void handleResponse(String path, MultiMap<String, String> params, HttpURLConnection connection,
            MultiMap<String, String> responseHeader) throws IOException, HttpException {
        int responseCode = connection.getResponseCode();
        String responseMsg = connection.getResponseMessage();
        this.extractCookies(connection);
        this.handleResponse(responseCode, responseMsg, path, params, extractError(connection, responseHeader));
    }

    /**
     * Extracts whatever came from the response body
     * 
     * @param connection {@link HttpURLConnection}
     * @param responseHeader {@link MultiMap}
     * @return {@link String}
     * @throws IOException e
     */
    private String extractError(HttpURLConnection connection, MultiMap<String, String> responseHeader) throws IOException {
        InputStream errorStream = connection.getErrorStream();
        if (errorStream == null) {
            return "";
        }
        String encoding = getCharset(responseHeader);
        return IOUtils.readFullyClose(new InputStreamReader(errorStream, encoding));
    }

    /**
     * Faz um wrap sobre o input stream da connection passada
     * 
     * @param connection {@link HttpURLConnection}
     * @return {@link InputStream}
     * @throws IOException e
     */
    protected InputStream getInputStream(HttpURLConnection connection) throws IOException {
        return connection.getInputStream();
    }

    /**
     * Permite a subclasses fazerem um wrap sobre o {@link OutputStream} antes de ser usado
     * 
     * @param output {@link OutputStream}
     * @return {@link OutputStream}
     * @throws IOException e
     */
    protected OutputStream wrapOutputStream(OutputStream output) throws IOException {
        return output;
    }

    /**
     * Extrai os cookies enviados pelo servidor e guarda no mapa {@link #cookies}
     * 
     * @param connection {@link HttpURLConnection}
     */
    private synchronized void extractCookies(HttpURLConnection connection) {
        Map<String, List<String>> headerFields = connection.getHeaderFields();
        if (headerFields != null) {
            List<String> list = headerFields.get("Set-Cookie");
            if (list != null) {
                for (String field : list) {
                    if (field != null) {
                        String[] split = field.split(",");
                        for (String string : split) {
                            String[] keyValue = this.stripAndSplit(string);
                            this.cookies.put(keyValue[0], keyValue[1]);
                        }
                    }
                }
            }
        }
    }

    /**
     * Quebra um cookie composto enviado pelo servidor, devolvendo apenas a chave e o valor
     * 
     * @param string String
     * @return String[]
     */
    private String[] stripAndSplit(String string) {
        String cookie = string;
        int index = cookie.indexOf(';');
        if (index >= 0) {
            cookie = cookie.substring(0, index);
        }
        return cookie.split("=");
    }

    /**
     * Seta os cookies presentes no campo {@link #cookies} na {@link HttpURLConnection}
     * 
     * @param connection {@link HttpURLConnection}
     */
    public synchronized void setCookies(HttpURLConnection connection) {
        StringBuilder sb = new StringBuilder();
        Iterator<Entry<String, String>> iterator = this.cookies.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<String, String> entry = iterator.next();
            sb.append(entry.getKey());
            sb.append('=');
            sb.append(entry.getValue());
            if (iterator.hasNext()) {
                sb.append(',');
            }
        }
        connection.setRequestProperty("Cookie", sb.toString());
    }

    /**
     * Abre a conexao com o servidor
     * 
     * @param path String
     * @param params parametros
     * @return {@link HttpURLConnection}
     * @throws IOException e
     * @throws HttpException e
     */
    protected HttpURLConnection openConnection(String path, MultiMap<String, String> params) throws IOException, HttpException {
        String queryParam = this.encodeQueryParameters(params);
        String thePath = this.prepend(path);
        String theURL = this.baseURL + thePath + (queryParam.length() > 0 ? "?" + queryParam : "");
        URL url = new URL(theURL);
        HttpURLConnection openConnection = (HttpURLConnection) url.openConnection();
        openConnection.setDoInput(true);
        setCookies(openConnection);
        return openConnection;
    }

    /**
     * Adiciona uma '/' no comeco da string se necessario
     * 
     * @param path String
     * @return String
     */
    private String prepend(String path) {
        if (!path.startsWith("/")) {
            return "/" + path;
        }
        return path;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Tuple<InputStream, MultiMap<String, String>> doMakeRequestWithBody(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters, InputStream requestBody)
            throws HttpException, IOException {
        HttpURLConnection openConnection = this.openConnection(path, queryParameters);
        try {
            openConnection.setRequestMethod(method.name());
            this.setRequestHeaders(openConnection, headerParameters);
            if (requestBody != null) {
                openConnection.setDoOutput(true);
                IOUtils.copyFullyClose(requestBody, this.wrapOutputStream(openConnection.getOutputStream()));
            }
            MultiMap<String, String> headerFields = extractResponseHeaderFields(openConnection);
            this.handleResponse(path, queryParameters, openConnection, headerFields);
            InputStream input = new TemporaryFileInputStream(this.getInputStream(openConnection));
            return UnmodifiableTuple.newTuple(input, headerFields);
        } finally {
            openConnection.disconnect();
        }
    }

    /**
     * Extrai a resposta do {@link HttpURLConnection}
     * 
     * @param openConnection {@link HttpURLConnection}
     * @return {@link MultiMap}
     */
    protected MultiMap<String, String> extractResponseHeaderFields(HttpURLConnection openConnection) {
        MultiMap<String, String> map = new ListMultiMap<String, String>();
        Set<Entry<String, List<String>>> set = openConnection.getHeaderFields().entrySet();
        for (Entry<String, List<String>> entry : set) {
            map.addAll(entry.getKey(), entry.getValue());
        }
        return map;
    }

    /**
     * Seta os campos do request header, concatenando os campos usando ';' como separador
     * 
     * @param openConnection a {@link HttpURLConnection}
     * @param headerParameters {@link MultiMap}
     */
    private void setRequestHeaders(HttpURLConnection openConnection, MultiMap<String, String> headerParameters) {
        if (headerParameters != null) {
            Collection<Entry<String, Collection<String>>> entries = headerParameters.entries();
            for (Entry<String, Collection<String>> entry : entries) {
                String key = entry.getKey();
                StringBuilder sb = new StringBuilder();
                Iterator<String> iterator = entry.getValue().iterator();
                while (iterator.hasNext()) {
                    String value = iterator.next();
                    sb.append(value);
                    if (iterator.hasNext()) {
                        sb.append(";");
                    }
                }
                openConnection.setRequestProperty(key, sb.toString());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public Tuple<InputStream, MultiMap<String, String>> makeRequest(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters) throws HttpException, IOException {
        return this.doMakeRequestWithBody(path, method, headerParameters, queryParameters, null);
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeRequestStringResponse(String path, HttpMethod method, MultiMap<String, String> headerParameters,
            MultiMap<String, String> queryParameters) throws HttpException, IOException {
        return this.makeRequestStringResponse(path, method, headerParameters, queryParameters, null);
    }

}
